/**
 *  Axios 实例, ajax请求底层方法
 *  官方文档 [https://github.com/axios/axios]{@link https://github.com/axios/axios}
 *  @module utils/axios
 */

import axios from 'axios';

import qs from 'qs';

import { compile } from 'path-to-regexp';

import { getHost } from './url';

// AJAX_SUCCESS
import { AJAX_SUCCESS } from '../config';

// store
import { store, updateOverallLoading } from '@s';

// 请求错误自定义
const errorCode = {
	401: '认证失败，无法访问系统资源',
	403: '当前操作没有权限',
	404: '访问资源不存在',
	default: '系统未知错误,请反馈给管理员'
};

// 编译过的url缓存
const pathToRegexCaches = {},
	httpPromiseArr = new Map();

/**
 * Axios实例化参数选项对象
 * @const
 * @type {object}
 * @property {object} headers 请求头对象对象，默认：null
 * @property {number} timeout 超时时间，默认：0， 不限制
 * @property {boolean} withCredentials 是否带上验证信息， 默认：true
 * @property {number} maxContentLength 限制最大发送内容长度，默认：-1 不限制
 * @property {strin} baseURL 默认请求拼接的前缀
 */
const service = axios.create({
	headers: {
		'Content-Type': 'application/json;charset=UTF-8',
		// ie浏览器get请求兼容问题
		'Cache-Control': 'no-cache',
		'If-Modified-Since': '0'
	},
	timeout: 0,
	withCredentials: false,
	responseType: 'json',
	maxContentLength: -1,
	baseURL: import.meta.env.VITE_APP_BASE_API
});

/**
 * 判断是否为JSON格式
 * @param {*} str
 * @returns
 */
function isJSON(str) {
	if (typeof str == 'string') {
		try {
			JSON.parse(str);
			return true;
		} catch (e) {
			return false;
		}
	}
}

/**
 * 创建缓存key, 由请求url、类型、参数、发送数据构成的标识符
 * @param {string} url 请求url
 * @param {string} type 请求类型
 * @param {object} params url参数对象
 * @param {object} data 请求数据
 * @return {string}
 */
function createKey(config) {
	const { url, method, params = {}, data = {} } = config;
	return encodeURIComponent([url, method, isJSON(params) ? params : JSON.stringify(params), isJSON(data) ? data : JSON.stringify(data)].join(','));
}

/**
 * 在请求发送数据之前，对发送数据进行转换
 */
service.interceptors.request.use(
	(config) => {
		// 是否需要设置 token
		const isToken = (config.headers || {}).isToken === false;

		const { token } = store.getState().base;

		if (token && !isToken) {
			// 让每个请求携带自定义token 请根据实际情况自行修改
			config.headers['Authorization'] = 'Bearer ' + token;
		}
		if (httpPromiseArr.size === 1) store.dispatch(updateOverallLoading(true));

		/**
		 * 请求未完成时保存取消的cancelToken，缓存正在进行的请求
		 */
		const key = createKey(config);
		config.cancelToken = new axios.CancelToken((cancel) => {
			if (httpPromiseArr.get(key)) httpPromiseArr.get(key).cancel = cancel;
		});

		// get请求参数转换
		if (config.method === 'get') config.paramsSerializer = (params) => qs.stringify(params, { arrayFormat: 'comma' });
		return config;
	},
	(error) => {
		message.error({
			content: '加载超时'
		});
		return Promise.reject(error);
	}
);

// 响应拦截器
service.interceptors.response.use(
	(res) => {
		// 未设置状态码则默认成功状态
		const code = res?.data?.code || AJAX_SUCCESS;
		// 获取错误信息
		const msg = errorCode[code] || res?.data?.msg || errorCode['default'];

		// 请求成功后从正在进行的请求数组中删除
		const key = createKey(res.config);
		let repeatArr = [];
		if (httpPromiseArr.has(key)) {
			repeatArr = httpPromiseArr.get(key).repeatArr;
			httpPromiseArr.delete(key);
		}

		// 全部请求结束关闭loading
		if (httpPromiseArr.size === 0)
			setTimeout(() => {
				store.dispatch(updateOverallLoading(false));
			}, 20);

		if (code === 401) {
			cancelFn();
			useNavigate()('/login');
		} else if (code !== 200) {
			message.error({ content: msg });
			for (const elem of repeatArr.values()) {
				elem[1](res.data);
			}
			return Promise.reject(res.data);
		} else {
			for (const elem of repeatArr.values()) {
				elem[0](res.data);
			}
			return Promise.resolve(res.data);
		}
	},
	// 服务器状态码不是200的情况
	(error) => {
		let { message: msg } = error;
		if (msg) {
			if (msg === 'Network Error') {
				msg = '后端接口连接异常';
			} else if (msg.includes('timeout')) {
				msg = '系统接口请求超时';
			} else if (msg.includes('Request failed with status code')) {
				msg = '系统接口' + msg.substr(msg.length - 3) + '异常';
			}
			message.error({ content: msg });
			store.dispatch(updateOverallLoading(false));
			cancelFn();
		}
		return Promise.reject(error);
	}
);

/**
 * 请求系统错误时 取消所有正在进行的请求函数
 * @returns
 */
export function cancelFn() {
	for (const item of httpPromiseArr.values()) {
		if (isFunction(item.cancel)) item.cancel();
	}
	httpPromiseArr.clear();
	store.dispatch(updateOverallLoading(false));
}

/**
 * Axios 实例
 * @example
 *
 *  // 基础用法
 *  import axios from '@/utils/axios'
 *  axios({
 *    method: 'post',
 *    url: '/user/123',
 *    data: {
 *      firstName: 'Fred',
 *      lastName: 'Flintstone'
 *    }
 *  })
 *
 *  @example
 *
 *  // 实例方法
 *  axios.request(config)
 *  axios.get(url[, config])
 *  axios.delete(url[, config])
 *  axios.head(url[, config])
 *  axios.options(url[, config])
 *  axios.post(url[, data[, config]])
 *  axios.put(url[, data[, config]])
 *  axios.patch(url[, data[, config]])
 */

/**
 *
 * @param {object} options
 * 请求配置参数
 * url请求地址必须传
 * method请求方法默认为get方法
 * data请求参数
 */

export default function (options) {
	// 处理默认参数，传参和默认参数合并
	const config = Object.assign({ method: 'get' }, options || {});

	// 必须要传入url
	if (!config.url) {
		throw new Error('ajax url is required!');
	}

	let { url, data } = config;
	const { method } = config;

	// page接口默认添加needCount：1，需要总数
	if (config.url.split('/').pop() === 'page' || config.url.split('/').pop() === 'pageSenior') {
		data = Object.assign({ needCount: 1 }, data || {});
	}
	// export接口默认responseType = 'blob'
	if (config.url.split('/').pop() === 'export') {
		config.responseType = 'blob';
	}

	// 从缓存中提取已经解析过的url
	// url 支持参数信息，如： /api/path/:id
	// 这种情况需要把url解析成一个正则表达式，然后再跟参数匹配组成一个真正要请求的url
	let compileCache = pathToRegexCaches[url];
	const host = getHost(url);
	if (!compileCache) {
		// 先排除host段，因为host段的端口号与参数写法有冲突
		compileCache = pathToRegexCaches[url] = compile(url.replace(host, ''), {});
	}
	// 出去传输过来的url参数，并补回host段
	url = host + compileCache(config.params);

	delete config.url;
	delete config.method;
	delete config.data;
	delete config.params;

	// 判断是否重复
	const key = ['get', 'head', 'delete'].includes(method)
		? createKey({ url, method, data: {}, params: data })
		: createKey({ url, method, data, params: {} });

	if (httpPromiseArr.has(key)) {
		return new Promise((resolve, reject) => {
			httpPromiseArr.get(key).repeatArr.push([resolve, reject]);
		});
	} else {
		httpPromiseArr.set(key, { cancel: '', repeatArr: [] });
		const http = ['get', 'head', 'delete'].includes(method)
			? service[method](url, {
					...config,
					params: data
				})
			: service[method](url, data, config);
		return http;
	}
}
